package org.spring.boot.func.proxy;

import org.spring.boot.func.annotation.FuncLambda;
import org.spring.boot.func.container.FuncMethod;
import org.spring.boot.func.factory.agent.FuncLink;
import org.spring.boot.func.tool.callback.FuncCallback;
import org.spring.boot.func.tool.callback.FuncCallbackError;
import org.spring.boot.func.tool.callback.FuncCallbackThen;
import org.spring.boot.func.tool.life.FuncLife;
import org.spring.boot.func.properties.FuncProperties;
import org.spring.boot.func.tool.life.FuncLifeEnd;
import org.spring.boot.func.tool.life.FuncLifeStart;
import org.spring.boot.func.tool.logger.FuncLogger;
import org.spring.boot.func.type.FuncToolType;
import org.spring.boot.func.utils.FuncString;
import org.spring.boot.func.type.FuncStringType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.util.Assert;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;

/**
 * @author Yiur
 */
public class FuncProxy implements InvocationHandler {

    private final static Logger logger = LoggerFactory.getLogger(FuncLambda.class);
    private ConfigurableListableBeanFactory beanFactory;
    private String beanName;
    private String[] refs;
    private List<FuncMethod> beanMethods;
    private Map<String, Object> beanContainer;
    private Class<?> classObject;
    private FuncProperties funcProperties;
    private FuncLink funcLink;

    public Object funcBind(Class<?> interfaceClass, Class<?> classObject, ConfigurableListableBeanFactory beanFactory, String beanName, String[] refs, List<FuncMethod> beanMethods) {
        this.classObject = classObject;
        this.beanContainer = new HashMap<>(2);
        this.beanFactory = beanFactory;
        this.beanName = beanName;
        this.refs = refs;
        this.beanMethods = beanMethods;
        this.funcProperties = this.beanFactory.getBean(FuncProperties.class);
        this.funcLink = this.beanFactory.getBean(FuncLink.class);
        return Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[] { interfaceClass, FuncLife.class }, this);
    }

    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        Assert.isTrue(funcBeanExits(), "Currently this bean does not exist");
        String currentMethodSource = methodName(method);
        FuncMethod funcMethod = funcMethod(currentMethodSource);

        String error = "Method: ?(?) --- Invalid calls to anonymous methods that are not implemented";
        if (funcMethod == null) {
            return null;
        }

        Object instance = instance();
        Method objectMethod = classObject.getMethod(method.getName(), method.getParameterTypes());

        logger(funcMethod);
        return life(funcMethod, instance, objectMethod, objects);
    }

    private Object instance() throws Throwable {
        Object instance = null;
        if (beanContainer.get(classObject.getName()) == null) {
            instance = classObject.newInstance();
            beanContainer.put(classObject.getName(), instance);
        }
        return beanContainer.get(classObject.getName());
    }

    private boolean funcBeanExits() {
        try {
            beanFactory.getBean(beanName);
            return true;
        } catch (BeansException e) {
            return false;
        }
    }

    private String methodName(Method method) {
        String currentMethodSource = method.toGenericString();
        if (!FuncString.matcher(FuncStringType.STRING_ARRAY_CHAR, currentMethodSource).equals(FuncStringType.STRING_EMPTY)) {
            currentMethodSource = currentMethodSource.replace(FuncStringType.STRING_ARRAY_CHAR, FuncStringType.STRING_ARRAY_TYPE);
        }
        return currentMethodSource;
    }

    private FuncMethod funcMethod(String currentMethodSource) {
        FuncMethod funcMethod = null;
        for (FuncMethod beanMethod : beanMethods) {
            if (FuncMethod.matcher(FuncMethod.REGEX_SOURCE, currentMethodSource).equals(beanMethod.getSource())) {
                funcMethod = beanMethod;
                break;
            }
        }
        return funcMethod;
    }

    private void logger(FuncMethod funcMethod) {
        if (funcMethod.getLogger().isEnableLog() || funcProperties.getLogger().isEnableLog()) {
            FuncLogger funcLogger = funcLink.getObject(FuncLogger.class, FuncString.format(FuncString.FUNC_LINK_FORMAT, beanName, FuncToolType.LOGGER_KEY.value));
            if (funcLogger == null) {
                for (String ref : refs) {
                    funcLogger = funcLink.getObject(FuncLogger.class, FuncString.format(FuncString.FUNC_LINK_FORMAT, ref, FuncToolType.LOGGER_KEY.value));
                    if (funcLogger != null) {
                        break;
                    }
                }
            }
            if (funcLogger != null) {
                logger.info(FuncString.replace(funcLogger.logger(FuncMethod.THIS_CONSTRAINT.keySet()), funcMethod, FuncMethod.THIS_CONSTRAINT));
            } else {
                logger.info(FuncString.replace(FuncProperties.DEFAULT_MESSAGE, funcMethod, FuncMethod.THIS_CONSTRAINT));
            }
        }
    }

    private Object callback(FuncMethod funcMethod, Object instance, Method objectMethod, Map<String, Object> parameter) throws Throwable {
        List<Object> objects = new ArrayList<>();
        for (String key : parameter.keySet()) {
            objects.add(parameter.get(key));
        }

        if (funcMethod.isCallback()) {
            FuncCallback funcCallback = null;
            if (funcLink.getObject(FuncCallback.class, FuncString.format(FuncString.FUNC_LINK_FORMAT, beanName, FuncToolType.CALLBACK_KEY.value)) != null) {
                funcCallback = funcLink.getObject(FuncCallback.class, FuncString.format(FuncString.FUNC_LINK_FORMAT, beanName, FuncToolType.CALLBACK_KEY.value));
            } else {
                for (String ref : refs) {
                    funcCallback = funcLink.getObject(FuncCallback.class, FuncString.format(FuncString.FUNC_LINK_FORMAT, ref, FuncToolType.CALLBACK_KEY.value));
                    if (funcCallback != null) {
                        break;
                    }
                }
            }

            FuncCallbackThen funcCallbackThen = funcLink.getObject(FuncCallbackThen.class, FuncString.format(FuncString.FUNC_LINK_FORMAT, beanName, FuncToolType.CALLBACK_THEN_KEY.value));
            FuncCallbackError funcCallbackError = funcLink.getObject(FuncCallbackError.class, FuncString.format(FuncString.FUNC_LINK_FORMAT, beanName, FuncToolType.CALLBACK_ERROR_KEY.value));

            if (funcCallbackThen != null) {
                try {
                    return funcCallbackThen.then(objectMethod.invoke(instance, objects.toArray()));
                } catch (Exception e) {
                    if (funcCallbackError != null) {
                        return funcCallbackError.error(e);
                    }
                }
            }

            if (funcCallback == null) {
                if (funcMethod.getCallbackClass() != FuncCallback.class) {
                    funcCallback = (FuncCallback) funcMethod.getCallbackClass().newInstance();
                } else {
                    funcCallback = (FuncCallback) funcProperties.getCallBack().newInstance();
                }
            }

            try {
                return funcCallback.then(objectMethod.invoke(instance, objects.toArray()));
            } catch (Exception e) {
                return funcCallback.error(e);
            }
        }
        return objectMethod.invoke(instance, objects.toArray());
    }

    private Object life(FuncMethod funcMethod, Object instance, Method objectMethod, Object[] objects) throws Throwable {
        Map<String, Object> parameter = new LinkedHashMap<>();
        List<String> parameterName = funcMethod.getParameterName();
        for (int i = 0; i < parameterName.size(); i++) {
            parameter.put(parameterName.get(i), objects[i]);
        }

        FuncLife funcLife = funcLink.getObject(FuncLife.class, FuncString.format(FuncString.FUNC_LINK_FORMAT, beanName, FuncToolType.LIFE_KEY.value));
        if (funcLife == null) {
            for (String ref : refs) {
                funcLife = funcLink.getObject(FuncLife.class, FuncString.format(FuncString.FUNC_LINK_FORMAT, ref, FuncToolType.LIFE_KEY.value));
                if (funcLife != null) {
                    break;
                }
            }
        }
        if (funcLife != null) {
            return funcLife.end(callback(funcMethod, instance, objectMethod, funcLife.start(parameter)));
        }

        FuncLifeStart funcLifeStart = funcLink.getObject(FuncLifeStart.class, FuncString.format(FuncString.FUNC_LINK_FORMAT, beanName, FuncToolType.LIFE_START_KEY.value));
        FuncLifeEnd funcLifeEnd = funcLink.getObject(FuncLifeEnd.class, FuncString.format(FuncString.FUNC_LINK_FORMAT, beanName, FuncToolType.LIFE_END_KEY.value));

        if (funcLifeStart != null) {
            parameter = funcLifeStart.start(parameter);
        }
        if (funcLifeEnd != null) {
            return funcLifeEnd.end(callback(funcMethod, instance, objectMethod, parameter));
        }

        return callback(funcMethod, instance, objectMethod, parameter);
    }

}
